<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h1 data-lake-id="HNX5j" id="HNX5j"><span data-lake-id="u7ff8a81c" id="u7ff8a81c">典型回答</span></h1>
  <p data-lake-id="u6fbb95c3" id="u6fbb95c3"><br></p>
  <p data-lake-id="u3f733b4a" id="u3f733b4a"><span data-lake-id="u58bee63f" id="u58bee63f">在系统设计中，快速失效(fail-fast)系统一种可以立即报告任何可能表明故障的情况的系统。快速失效系统通常设计用于停止正常操作，而不是试图继续可能存在缺陷的过程。</span></p>
  <p data-lake-id="ud767817d" id="ud767817d"><span data-lake-id="u3cd552a1" id="u3cd552a1">​</span><br></p>
  <p data-lake-id="u3ab062f8" id="u3ab062f8"><span data-lake-id="u854a1742" id="u854a1742">其实，这是一种理念，说白了就是在做系统设计的时候先考虑异常情况，一旦发生异常，直接停止并上报。</span></p>
  <p data-lake-id="ue4f5c305" id="ue4f5c305"><span data-lake-id="u5f2c65db" id="u5f2c65db">举一个最简单的fail-fast的例子：</span></p>
  <p data-lake-id="uc4160c01" id="uc4160c01"><span data-lake-id="ufca4447b" id="ufca4447b">​</span><br></p>
  <pre lang="java"><code>
public int divide(int dividend,int divisor){
    if(divisor == 0){
        throw new RuntimeException("divisor can't be zero");
    }
    return dividend/divisor;
}
</code></pre>
  <p data-lake-id="u193f71d0" id="u193f71d0"><span data-lake-id="u94c7c1ea" id="u94c7c1ea"> </span></p>
  <p data-lake-id="u97b82246" id="u97b82246"><span data-lake-id="u3b15a76d" id="u3b15a76d">上面的代码是一个对两个整数做除法的方法，在divide方法中，我们对除数做了个简单的检查，如果其值为0，那么就直接抛出一个异常，并明确提示异常原因。这其实就是fail-fast理念的实际应用。</span></p>
  <p data-lake-id="u4a9ba49d" id="u4a9ba49d"><span data-lake-id="ubf9b3aeb" id="ubf9b3aeb">​</span><br></p>
  <p data-lake-id="ue7874b73" id="ue7874b73"><span data-lake-id="u2009c39e" id="u2009c39e">这样做的好处就是可以预先识别出一些错误情况，一方面可以避免执行复杂的其他代码，另外一方面，这种异常情况被识别之后也可以针对性的做一些单独处理。</span></p>
  <p data-lake-id="u707f3052" id="u707f3052"><span data-lake-id="u8b2c5493" id="u8b2c5493">​</span><br></p>
  <p data-lake-id="u7a793ea3" id="u7a793ea3"><span data-lake-id="u13bf001f" id="u13bf001f">在Java中，集合类中有用到fail-fast机制进行设计，一旦使用不当，触发fail-fast机制设计的代码，就会发生非预期情况。</span></p>
  <p data-lake-id="u01ac3c09" id="u01ac3c09"><span data-lake-id="u1513dad8" id="u1513dad8">​</span><br></p>
  <p data-lake-id="ufd20096a" id="ufd20096a"><span data-lake-id="u849dd8cc" id="u849dd8cc">在集合类中，为了避免并发修改，会维护一个expectedModCount属性，他表示这个迭代器预期该集合被修改的次数。还有一个modCount属性，他表示该集合实际被修改的次数。在集合被修改时，会去比较modCount和expectedModCount的值，如果不一致，则会触发fail-fast机制，抛出ConcurrentModificationException。</span></p>
  <p data-lake-id="u83f3f133" id="u83f3f133"><span data-lake-id="ub5bb91d0" id="ub5bb91d0">​</span><br></p>
  <p data-lake-id="ua714df1d" id="ua714df1d"><span data-lake-id="uad8defe6" id="uad8defe6">fail-safe 机制是为线程安全的集合准备的，可以避免像 fail-fast 一样在并发使用集合的时候，不断地抛出异常。 </span></p>
  <h1 data-lake-id="hjJA9" id="hjJA9"><span data-lake-id="u254a7a56" id="u254a7a56">扩展知识</span></h1>
  <h2 data-lake-id="oymdf" id="oymdf"><span data-lake-id="ud94c1c42" id="ud94c1c42">集合类中的fail-fast</span></h2>
  <p data-lake-id="u7a199eed" id="u7a199eed"><span data-lake-id="ud6da825e" id="ud6da825e">我们通常说的Java中的fail-fast机制，默认指的是Java集合的一种错误检测机制。当多个线程对部分集合进行结构上的改变的操作时，有可能会产生fail-fast机制，这个时候就会抛出ConcurrentModificationException</span></p>
  <p data-lake-id="ubc7afa2d" id="ubc7afa2d"><span data-lake-id="u3335200c" id="u3335200c">​</span><br></p>
  <p data-lake-id="u1c9d5f52" id="u1c9d5f52"><span data-lake-id="u698b3821" id="u698b3821">ConcurrentModificationException，当方法检测到对象的并发修改，但不允许这种修改时就抛出该异常。</span></p>
  <p data-lake-id="u15f95adf" id="u15f95adf"><span data-lake-id="u3d06c184" id="u3d06c184">​</span><br></p>
  <p data-lake-id="u7ca519fe" id="u7ca519fe"><span data-lake-id="u6d8ed84b" id="u6d8ed84b">在Java中， 如果在foreach 循环里对某些集合元素进行元素的 remove/add 操作的时候，就会触发fail-fast机制，进而抛出ConcurrentModificationException。</span></p>
  <p data-lake-id="u913f4ef5" id="u913f4ef5"><span data-lake-id="ubeebfcba" id="ubeebfcba">​</span><br></p>
  <p data-lake-id="ud1cc4906" id="ud1cc4906"><span data-lake-id="u1e045d75" id="u1e045d75">如以下代码：</span></p>
  <pre lang="java"><code>
List&lt;String&gt; userNames = new ArrayList&lt;String&gt;() {{
    add("Hollis");
    add("hollis");
    add("HollisChuang");
    add("H");
}};

for (String userName : userNames) {
    if (userName.equals("Hollis")) {
        userNames.remove(userName);
    }
}

System.out.println(userNames);
</code></pre>
  <p data-lake-id="ua799f56b" id="ua799f56b"><span data-lake-id="ud335ac70" id="ud335ac70">以上代码，使用增强for循环遍历元素，并尝试删除其中的Hollis字符串元素。运行以上代码，会抛出以下异常：</span></p>
  <pre lang="java"><code>
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at com.hollis.ForEach.main(ForEach.java:22)
</code></pre>
  <p data-lake-id="u2cf79d4a" id="u2cf79d4a"><br></p>
  <p data-lake-id="u3751a9a5" id="u3751a9a5"><span data-lake-id="u4572d3c9" id="u4572d3c9">同样的，读者可以尝试下在增强for循环中使用add方法添加元素，结果也会同样抛出该异常。</span></p>
  <p data-lake-id="u6c3cdb0a" id="u6c3cdb0a"><span data-lake-id="u5f93abc8" id="u5f93abc8">在深入原理之前，我们先尝试把foreach进行解语法糖，看一下foreach具体如何实现的。</span></p>
  <p data-lake-id="u668603de" id="u668603de"><span data-lake-id="ucec69d52" id="ucec69d52">我们使用jad工具，对编译后的class进行反编译，得到以下代码：</span></p>
  <p data-lake-id="uf3aff413" id="uf3aff413"><br></p>
  <pre lang="java"><code>
public static void main(String[] args) {
    // 使用ImmutableList初始化一个List
    List&lt;String&gt; userNames = new ArrayList&lt;String&gt;() {{
        add("Hollis");
        add("hollis");
        add("HollisChuang");
        add("H");
    }};

    Iterator iterator = userNames.iterator();
    do
        {
            if(!iterator.hasNext())
                break;
            String userName = (String)iterator.next();
            if(userName.equals("Hollis"))
                userNames.remove(userName);
        } while(true);
    System.out.println(userNames);
}
</code></pre>
  <p data-lake-id="uc211de14" id="uc211de14"><span data-lake-id="uf66dd974" id="uf66dd974">可以发现，foreach其实是依赖了while循环和Iterator实现的。</span></p>
  <h3 data-lake-id="e36Ix" id="e36Ix"><span data-lake-id="u2924be5e" id="u2924be5e">异常原理</span></h3>
  <p data-lake-id="uc0ecc93a" id="uc0ecc93a"><span data-lake-id="u182142dc" id="u182142dc">通过以上代码的异常堆栈，我们可以跟踪到真正抛出异常的代码是：</span></p>
  <pre lang="java"><code>
java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
</code></pre>
  <p data-lake-id="uc959958f" id="uc959958f"><span data-lake-id="u2d517d20" id="u2d517d20">该方法是在iterator.next()方法中调用的。我们看下该方法的实现：</span></p>
  <pre lang="java"><code>
final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}
</code></pre>
  <p data-lake-id="uf64ec918" id="uf64ec918"><br></p>
  <p data-lake-id="ud9579c98" id="ud9579c98"><span data-lake-id="u7a295aaa" id="u7a295aaa">如上，在该方法中对modCount和expectedModCount进行了比较，如果二者不相等，则抛出ConcurrentModificationException。</span></p>
  <p data-lake-id="udeee3dea" id="udeee3dea"><span data-lake-id="u7c4881ed" id="u7c4881ed">那么，modCount和expectedModCount是什么？是什么原因导致他们的值不相等的呢？</span></p>
  <p data-lake-id="ued833785" id="ued833785"><span data-lake-id="ub2950765" id="ub2950765">modCount是ArrayList中的一个成员变量。它表示该集合实际被修改的次数。</span></p>
  <pre lang="java"><code>
List&lt;String&gt; userNames = new ArrayList&lt;String&gt;() {{
    add("Hollis");
    add("hollis");
    add("HollisChuang");
    add("H");
}};
</code></pre>
  <p data-lake-id="u14d6bb71" id="u14d6bb71"><br></p>
  <p data-lake-id="ub31ecc0c" id="ub31ecc0c"><span data-lake-id="u14cea5d4" id="u14cea5d4">当使用以上代码初始化集合之后该变量就有了。初始值为0。</span></p>
  <p data-lake-id="u6c921f77" id="u6c921f77"><span data-lake-id="u33170d58" id="u33170d58">expectedModCount 是 ArrayList中的一个内部类——Itr中的成员变量。</span></p>
  <pre lang="java"><code>
Iterator iterator = userNames.iterator();
</code></pre>
  <p data-lake-id="u9cc85f7d" id="u9cc85f7d"><br></p>
  <p data-lake-id="u5ea263d2" id="u5ea263d2"><span data-lake-id="u23f4b723" id="u23f4b723">以上代码，即可得到一个 Itr类，该类实现了Iterator接口。</span></p>
  <p data-lake-id="u675f6903" id="u675f6903"><span data-lake-id="u653705bf" id="u653705bf">expectedModCount表示这个迭代器预期该集合被修改的次数。其值随着Itr被创建而初始化。只有通过迭代器对集合进行操作，该值才会改变。</span></p>
  <p data-lake-id="u2dd801f3" id="u2dd801f3"><span data-lake-id="u8a4024d7" id="u8a4024d7">那么，接着我们看下userNames.remove(userName);方法里面做了什么事情，为什么会导致expectedModCount和modCount的值不一样。</span></p>
  <p data-lake-id="u10a9a744" id="u10a9a744"><span data-lake-id="u7f307a91" id="u7f307a91">通过翻阅代码，我们也可以发现，remove方法核心逻辑如下：</span></p>
  <p data-lake-id="uf27e9a53" id="uf27e9a53"><br></p>
  <pre lang="java"><code>
private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved &gt; 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work
}
</code></pre>
  <p data-lake-id="u31bdc4bf" id="u31bdc4bf"><br></p>
  <p data-lake-id="udb1598c2" id="udb1598c2"><span data-lake-id="uf54e9211" id="uf54e9211">可以看到，它只修改了modCount，并没有对expectedModCount做任何操作。</span></p>
  <p data-lake-id="ub917dbed" id="ub917dbed"><span data-lake-id="ud0da1fe0" id="ud0da1fe0">简单画一张图描述下以上场景：</span></p>
  <p data-lake-id="u7bd17ead" id="u7bd17ead"><br></p>
  <p data-lake-id="ubff74f1e" id="ubff74f1e"><img src="http://www.hollischuang.com/wp-content/uploads/2019/04/15551448234429.jpg?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_24%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"><span data-lake-id="u344e6846" id="u344e6846">​</span></p>
  <p data-lake-id="u0487269c" id="u0487269c"><br></p>
  <p data-lake-id="u7bf33bba" id="u7bf33bba"><span data-lake-id="u3e9c1f76" id="u3e9c1f76">简单总结一下，之所以会抛出CMException异常，是因为我们的代码中使用了增强for循环，而在增强for循环中，集合遍历是通过iterator进行的，但是元素的add/remove却是直接使用的集合类自己的方法。这就导致iterator在遍历的时候，会发现有一个元素在自己不知不觉的情况下就被删除/添加了，就会抛出一个异常，用来提示用户，可能发生了并发修改！</span></p>
  <p data-lake-id="u37f8c75b" id="u37f8c75b"><span data-lake-id="u6b442903" id="u6b442903">所以，在使用Java的集合类的时候，如果发生CMException，优先考虑fail-fast有关的情况，实际上这里并没有真的发生并发，只是Iterator使用了fail-fast的保护机制，只要他发现有某一次修改是未经过自己进行的，那么就会抛出异常。</span></p>
  <p data-lake-id="u29c5fb3f" id="u29c5fb3f"><br></p>
  <h2 data-lake-id="x1xJQ" id="x1xJQ"><span data-lake-id="ua7cf050c" id="ua7cf050c">fail-safe</span></h2>
  <p data-lake-id="uceab06bb" id="uceab06bb"><span data-lake-id="ue1606c60" id="ue1606c60">为了避免触发fail-fast机制，导致异常，我们可以使用Java中提供的一些采用了fail-safe机制的集合类。</span></p>
  <p data-lake-id="uca07cbc8" id="uca07cbc8"><span data-lake-id="u422bb114" id="u422bb114">这样的集合容器在遍历时不是直接在集合内容上访问的，而是先复制原有集合内容，在拷贝的集合上进行遍历。</span></p>
  <p data-lake-id="ub736d3b0" id="ub736d3b0"><span data-lake-id="ufdfe9f21" id="ufdfe9f21">java.util.concurrent包下的容器都是fail-safe的，可以在多线程下并发使用，并发修改。同时也可以在foreach中进行add/remove 。</span></p>
  <p data-lake-id="u4c81d938" id="u4c81d938"><span data-lake-id="u49f973bc" id="u49f973bc">我们拿CopyOnWriteArrayList这个fail-safe的集合类来简单分析一下。</span></p>
  <p data-lake-id="uab3ef741" id="uab3ef741"><br></p>
  <pre lang="java"><code>
public static void main(String[] args) {
    List&lt;String&gt; userNames = new CopyOnWriteArrayList&lt;String&gt;() {{
        add("Hollis");
        add("hollis");
        add("HollisChuang");
        add("H");
    }};

    userNames.iterator();

    for (String userName : userNames) {
        if (userName.equals("Hollis")) {
            userNames.remove(userName);
        }
    }

    System.out.println(userNames);
}
</code></pre>
  <p data-lake-id="ucae5c1af" id="ucae5c1af"><br></p>
  <p data-lake-id="u9bee3aca" id="u9bee3aca"><span data-lake-id="udf193752" id="udf193752">以上代码，使用CopyOnWriteArrayList代替了ArrayList，就不会发生异常。</span></p>
  <p data-lake-id="u0aa8d962" id="u0aa8d962"><span data-lake-id="u157496a9" id="u157496a9">fail-safe集合的所有对集合的修改都是先拷贝一份副本，然后在副本集合上进行的，并不是直接对原集合进行修改。并且这些修改方法，如add/remove都是通过加锁来控制并发的。</span></p>
  <p data-lake-id="u79053be4" id="u79053be4"><span data-lake-id="u41d596e4" id="u41d596e4">所以，CopyOnWriteArrayList中的迭代器在迭代的过程中不需要做fail-fast的并发检测。（因为fail-fast的主要目的就是识别并发，然后通过异常的方式通知用户）</span></p>
  <p data-lake-id="u2a22c340" id="u2a22c340"><span data-lake-id="u6149f25e" id="u6149f25e">但是，虽然基于拷贝内容的优点是避免了ConcurrentModificationException，但同样地，迭代器并不能访问到修改后的内容。如以下代码：</span></p>
  <pre lang="java"><code>
public static void main(String[] args) {
    List&lt;String&gt; userNames = new CopyOnWriteArrayList&lt;String&gt;() {{
        add("Hollis");
        add("hollis");
        add("HollisChuang");
        add("H");
    }};

    Iterator it = userNames.iterator();

    for (String userName : userNames) {
        if (userName.equals("Hollis")) {
            userNames.remove(userName);
        }
    }

    System.out.println(userNames);

    while(it.hasNext()){
        System.out.println(it.next());
    }
}
</code></pre>
  <p data-lake-id="ufef33c39" id="ufef33c39"><br></p>
  <p data-lake-id="u21bf086b" id="u21bf086b"><span data-lake-id="u2cc2bd63" id="u2cc2bd63">我们得到CopyOnWriteArrayList的Iterator之后，通过for循环直接删除原数组中的值，最后在结尾处输出Iterator，结果发现内容如下：</span></p>
  <p data-lake-id="u870c84a1" id="u870c84a1"><br></p>
  <pre lang="java"><code>
[hollis, HollisChuang, H]
Hollis
hollis
HollisChuang
H
</code></pre>
  <p data-lake-id="ubf8aa681" id="ubf8aa681"><br></p>
  <p data-lake-id="ud060a39e" id="ud060a39e"><span data-lake-id="ua03ecea7" id="ua03ecea7">迭代器遍历的是开始遍历那一刻拿到的集合拷贝，在遍历期间原集合发生的修改迭代器是不知道的。</span></p>
  <h2 data-lake-id="Dmvs5" id="Dmvs5"><span data-lake-id="u01e097b9" id="u01e097b9">什么是Copy-On-Write</span></h2>
  <p data-lake-id="u11f26d8f" id="u11f26d8f"><span data-lake-id="u7532529e" id="u7532529e">在了解了CopyOnWriteArrayList之后，不知道大家会不会有这样的疑问：他的add/remove等方法都已经加锁了，还要copy一份再修改干嘛？多此一举？同样是线程安全的集合，这玩意和Vector有啥区别呢？</span></p>
  <p data-lake-id="u90a28b33" id="u90a28b33"><span data-lake-id="u884fae77" id="u884fae77">Copy-On-Write简称COW，是一种用于程序设计中的优化策略。其基本思路是，从一开始大家都在共享同一个内容，当某个人想要修改这个内容的时候，才会真正把内容Copy出去形成一个新的内容然后再改，这是一种延时懒惰策略。</span></p>
  <p data-lake-id="u5c631265" id="u5c631265"><span data-lake-id="uf7def209" id="uf7def209">CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候，不直接往当前容器添加，而是先将当前容器进行Copy，复制出一个新的容器，然后新的容器里添加元素，添加完元素之后，再将原容器的引用指向新的容器。</span></p>
  <p data-lake-id="u30c7538a" id="u30c7538a"><span data-lake-id="u337da0ab" id="u337da0ab">CopyOnWriteArrayList中add/remove等写方法是需要加锁的，目的是为了避免Copy出N个副本出来，导致并发写。</span></p>
  <p data-lake-id="u9cba21ce" id="u9cba21ce"><span data-lake-id="ubc4db0ad" id="ubc4db0ad">但是，CopyOnWriteArrayList中的读方法是没有加锁的。</span></p>
  <p data-lake-id="ud1b6c436" id="ud1b6c436"><br></p>
  <pre lang="java"><code>
public E get(int index) {
    return get(getArray(), index);
}
</code></pre>
  <p data-lake-id="u9a56344c" id="u9a56344c"><br></p>
  <p data-lake-id="u90f1a564" id="u90f1a564"><span data-lake-id="u919bdd35" id="u919bdd35">这样做的好处是我们可以对CopyOnWrite容器进行并发的读，当然，这里读到的数据可能不是最新的。因为写时复制的思想是通过延时更新的策略来实现数据的最终一致性的，并非强一致性。</span></p>
  <p data-lake-id="ufb7d5e6d" id="ufb7d5e6d"><strong><span data-lake-id="u3380f598" id="u3380f598">所以CopyOnWrite容器是一种读写分离的思想，读和写不同的容器。</span></strong><span data-lake-id="uc0658bc9" id="uc0658bc9">而Vector在读写的时候使用同一个容器，读写互斥，同时只能做一件事儿。</span></p>
 </body>
</html>